/*
 * Copyright 2008 M-Way Solutions
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.mwaysolutions.ocm.impl;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Calendar;
import java.util.Date;
import java.util.Map;

import javax.jcr.NamespaceRegistry;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.Property;
import javax.jcr.PropertyIterator;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Value;
import javax.jcr.ValueFactory;
import javax.jcr.ValueFormatException;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.NameFactory;

import com.mwaysolutions.ocm.OcmException;

/**
 *
 * @author <a href="mailto:m.zdila@mwaysolutions.com">Martin Ždila</a>
 */
public class OcmUtils {

	private static final Log log = LogFactory.getLog(OcmUtils.class);


	public static Value getValue(final Object value, final ValueFactory valueFactory) {
		if (value == null) {
			return null;
		}

		if (value instanceof String) {
			return valueFactory.createValue((String) value);
		} else if (value instanceof Integer) {
			return valueFactory.createValue((Integer) value);
		} else if (value instanceof Long) {
			return valueFactory.createValue((Long) value);
		} else if (value instanceof Short) {
			return valueFactory.createValue((Short) value);
		} else if (value instanceof Double) {
			return valueFactory.createValue((Double) value);
		} else if (value instanceof Float) {
			return valueFactory.createValue((Float) value);
		} else if (value instanceof Byte) {
			return valueFactory.createValue((Byte) value);
		} else if (value instanceof Boolean) {
			return valueFactory.createValue((Boolean) value);
		} else if (value instanceof Date) {
			final Calendar cal = Calendar.getInstance();
			cal.setTime((Date) value);
			return valueFactory.createValue(cal);
		} else if (value instanceof Calendar) {
			return valueFactory.createValue((Calendar) value);
		} else if (value instanceof InputStream) {
			return valueFactory.createValue((InputStream) value);
		} else if (value instanceof byte[]) {
			return valueFactory.createValue(new ByteArrayInputStream((byte[]) value));
		} else if (value instanceof Enum) {
			return valueFactory.createValue(((Enum<?>) value).name());
		} else {
			throw new IllegalArgumentException("Unsupported field type: " + value.getClass().getName());
		}
	}


	public static boolean isSetter(final Method method) {
		final String methodName = method.getName();
		final boolean returnsVoid = method.getReturnType().equals(void.class);
		final int argLen = method.getParameterTypes().length;

		if (methodName.startsWith("set") && returnsVoid && argLen == 1) {
			return true;
		} else if ((methodName.startsWith("get") || methodName.startsWith("is")) && !returnsVoid && argLen == 0) {
			return false;
		} else {
			throw new IllegalArgumentException(method.toString() + " is not getter nor setter");
		}
	}


	public static Object getValue(final Property property) throws RepositoryException {
		if (property.getDefinition().isMultiple()) {
			final Value[] values = property.getValues();
			switch (property.getType()) {
			case PropertyType.STRING:
			case PropertyType.PATH:
			case PropertyType.REFERENCE:
			case PropertyType.NAME:
				final String[] stringArr = (String[]) Array.newInstance(String.class, values.length);
				for (int i = 0; i < values.length; i++) {
					stringArr[i] = values[i].getString();
				}
				return stringArr;
			case PropertyType.LONG:
				final long[] longArr = (long[]) Array.newInstance(long.class, values.length);
				for (int i = 0; i < values.length; i++) {
					longArr[i] = values[i].getLong();
				}
				return longArr;
			case PropertyType.DOUBLE:
				final double[] doubleArr = (double[]) Array.newInstance(double.class, values.length);
				for (int i = 0; i < values.length; i++) {
					doubleArr[i] = values[i].getDouble();
				}
				return doubleArr;
			case PropertyType.BOOLEAN:
				final boolean[] booleanArr = (boolean[]) Array.newInstance(boolean.class, values.length);
				for (int i = 0; i < values.length; i++) {
					booleanArr[i] = values[i].getBoolean();
				}
				return booleanArr;
			case PropertyType.BINARY:
				final InputStream[] streamArr = (InputStream[]) Array.newInstance(InputStream.class, values.length);
				for (int i = 0; i < values.length; i++) {
					streamArr[i] = values[i].getStream();
				}
				return streamArr;
			case PropertyType.DATE:
				final Date[] dateArr = (Date[]) Array.newInstance(Calendar.class, values.length);
				for (int i = 0; i < values.length; i++) {
					dateArr[i] = values[i].getDate().getTime();
				}
				return dateArr;
			default:
				throw new IllegalArgumentException("Unsupported Value Type");
			}
		}

		final Value value = property.getValue();
		switch (value.getType()) {
		case PropertyType.STRING:
		case PropertyType.PATH:
		case PropertyType.REFERENCE:
		case PropertyType.NAME:
			return value.getString();
		case PropertyType.LONG:
			return value.getLong();
		case PropertyType.DOUBLE:
			return value.getDouble();
		case PropertyType.BOOLEAN:
			return value.getBoolean();
		case PropertyType.BINARY:
			return value.getStream();
		case PropertyType.DATE:
			return value.getDate().getTime();
		default:
			throw new IllegalArgumentException("Unsupported Value Type");
		}
	}


	public static void dump(final Node node, final int level) throws RepositoryException {
		final String padding = StringUtils.repeat("  ", level);
		System.out.println(padding + node.getName());

		for (final PropertyIterator pi = node.getProperties(); pi.hasNext();) {
			final Property property = pi.nextProperty();
			try {
				System.out.print(padding + "  @" + property.getName() + " = ");
				System.out.println(property.getValue().getString());
			} catch (final ValueFormatException e) {
				System.out.print("{");
				boolean has = false;
				for (final Value value : property.getValues()) {
					if (has) {
						System.out.print(", ");
					} else {
						has = true;
					}
					System.out.print(value.getString());
				}
				System.out.println("}");
			}
		}

		for (final NodeIterator ni = node.getNodes(); ni.hasNext();) {
			final Node childNode = ni.nextNode();
			dump(childNode, level + 1);
		}
	}


	static String getPropertyName(final String getterOrSetterName) {
		if (getterOrSetterName.startsWith("get")) {
			return Character.toLowerCase(getterOrSetterName.charAt(3)) + getterOrSetterName.substring(4);
		} else if (getterOrSetterName.startsWith("is")) {
			return Character.toLowerCase(getterOrSetterName.charAt(2)) + getterOrSetterName.substring(3);
		} else if (getterOrSetterName.startsWith("set")) {
			return Character.toLowerCase(getterOrSetterName.charAt(3)) + getterOrSetterName.substring(4);
		} else {
			throw new IllegalArgumentException(getterOrSetterName + " is not valid name of getter or setter");
		}
	}


	static void setProperty(final Object object, final String name, final Object value) {
		final String methodName = "set" + StringUtils.capitalize(name);
		found: {
			methodLoop: for (final Method method : object.getClass().getMethods()) {
				if (!method.getName().equals(methodName)) {
					continue;
				}

				final Class<?>[] parameterTypes = method.getParameterTypes();
				if (parameterTypes.length != 1) {
					continue;
				}

				try {
					if (value == null) {
						method.invoke(object, value);
						break found;
					}

					final Class<?> valueClass = value.getClass();
					final Class<?> parameterType = parameterTypes[0];

					if (parameterType.isAssignableFrom(valueClass) ||
							valueClass == Boolean.class && parameterType == boolean.class) {
						method.invoke(object, value);
						break found;
					} else if (valueClass == Double.class) {
						if (parameterType.isAssignableFrom(Float.class) || parameterType == float.class) {
							method.invoke(object, ((Number) value).floatValue());
							break found;
						} else if (/*parameterType.isAssignableFrom(Double.class) || */parameterType == double.class) {
							method.invoke(object, ((Number) value).doubleValue());
							break found;
						}
					} else if (valueClass == Long.class) {
						if (parameterType.isAssignableFrom(Short.class) || parameterType == short.class) {
							method.invoke(object, ((Number) value).shortValue());
							break found;
						} else if (parameterType.isAssignableFrom(Integer.class) || parameterType == int.class) {
							method.invoke(object, ((Number) value).intValue());
							break found;
						} else if (/*parameterType.isAssignableFrom(Long.class) || */parameterType == long.class) {
							method.invoke(object, ((Number) value).longValue());
							break found;
						} else if (parameterType.isAssignableFrom(Byte.class) || parameterType == byte.class) {
							method.invoke(object, ((Number) value).byteValue());
							break found;
						}
					} else if (valueClass == boolean[].class && parameterType.isAssignableFrom(Boolean[].class)) {
						final Object[] parameter = new Boolean[Array.getLength(value)];
						for (int i = 0; i < parameter.length; i++) {
							parameter[i] = Array.get(value, i);
						}
						method.invoke(object, (Object) parameter);
						break found;
					} else if (valueClass == double[].class) {
						if (parameterType == float[].class) {
							final float[] parameter = new float[Array.getLength(value)];
							for (int i = 0; i < parameter.length; i++) {
								parameter[i] = ((Double) Array.get(value, i)).floatValue();
							}
							method.invoke(object, parameter);
							break found;
						} else if (parameterType == Float[].class) {
							final Float[] parameter = new Float[Array.getLength(value)];
							for (int i = 0; i < parameter.length; i++) {
								parameter[i] = ((Double) Array.get(value, i)).floatValue();
							}
							method.invoke(object, (Object) parameter);
							break found;
						/*} else if (parameterType == double[].class) {*/
						} else if (parameterType == Double[].class) {
							final Double[] parameter = new Double[Array.getLength(value)];
							for (int i = 0; i < parameter.length; i++) {
								parameter[i] = ((Double) Array.get(value, i)).doubleValue();
							}
							method.invoke(object, (Object) parameter);
							break found;
						}
					} else if (valueClass == long[].class) {
						if (parameterType == byte[].class) {
							final byte[] parameter = new byte[Array.getLength(value)];
							for (int i = 0; i < parameter.length; i++) {
								parameter[i] = ((Long) Array.get(value, i)).byteValue();
							}
							method.invoke(object, parameter);
							break found;
						} else if (parameterType == Byte[].class) {
							final Byte[] parameter = new Byte[Array.getLength(value)];
							for (int i = 0; i < parameter.length; i++) {
								parameter[i] = ((Long) Array.get(value, i)).byteValue();
							}
							method.invoke(object, (Object) parameter);
							break found;
						} else if (parameterType == short[].class) {
							final short[] parameter = new short[Array.getLength(value)];
							for (int i = 0; i < parameter.length; i++) {
								parameter[i] = ((Long) Array.get(value, i)).shortValue();
							}
							method.invoke(object, parameter);
							break found;
						} else if (parameterType == Short[].class) {
							final Short[] parameter = new Short[Array.getLength(value)];
							for (int i = 0; i < parameter.length; i++) {
								parameter[i] = ((Long) Array.get(value, i)).shortValue();
							}
							method.invoke(object, (Object) parameter);
							break found;
						} else if (parameterType == int[].class) {
							final int[] parameter = new int[Array.getLength(value)];
							for (int i = 0; i < parameter.length; i++) {
								parameter[i] = ((Long) Array.get(value, i)).intValue();
							}
							method.invoke(object, parameter);
							break found;
						} else if (parameterType == Integer[].class) {
							final Integer[] parameter = new Integer[Array.getLength(value)];
							for (int i = 0; i < parameter.length; i++) {
								parameter[i] = ((Long) Array.get(value, i)).intValue();
							}
							method.invoke(object, (Object) parameter);
							break found;
						/*} else if (parameterType == long[].class) {*/
						} else if (parameterType == Long[].class) {
							final Long[] parameter = new Long[Array.getLength(value)];
							for (int i = 0; i < parameter.length; i++) {
								parameter[i] = ((Long) Array.get(value, i)).longValue();
							}
							method.invoke(object, (Object) parameter);
							break found;
						}
					} else if (valueClass.isArray() && parameterType.isArray()) { // try to convert Object[] to specific array
						final int len = Array.getLength(value);
						final Object parameter = Array.newInstance(parameterType.getComponentType(), len);
						for (int i = 0; i < len; i++) {
							try {
								Array.set(parameter, i, Array.get(value, i));
							} catch (final IllegalArgumentException e) {
								continue methodLoop;
							}
						}
						method.invoke(object, parameter);
						break found;
					} else if (InputStream.class.isAssignableFrom(valueClass) && parameterType == byte[].class) {
						try {
							method.invoke(object, IOUtils.toByteArray(((InputStream) value)));
						} catch (final IOException e) {
							throw new OcmException(e);
						}
						break found;
					} else if (Calendar.class.isAssignableFrom(valueClass) && parameterType.isAssignableFrom(Date.class)) {
						method.invoke(object, ((Calendar) value).getTime());
						break found;
					} else if (valueClass == String.class && Enum.class.isAssignableFrom(parameterType)) {
						method.invoke(object, createEnum(parameterType, (String) value));
						break found;
					}
				} catch (final IllegalArgumentException e) {
					throw new OcmException(e);
				} catch (final IllegalAccessException e) {
					throw new OcmException(e);
				} catch (final InvocationTargetException e) {
					throw new OcmException(e);
				}
			}

			log.warn("could not find appropriate setter method for property " + name + " and value " +
					value.getClass().getCanonicalName() + " in " + object.getClass().getCanonicalName() + " class");

		} // found
	}


	@SuppressWarnings("unchecked")
	private static Enum createEnum(final Class parameterType, final String value) {
		return Enum.valueOf(parameterType, value);
	}


	static Object getProperty(final Object object, final String name) {
		try {
			return PropertyUtils.getSimpleProperty(object, name);
		} catch (final IllegalAccessException e) {
			throw new OcmException(e);
		} catch (final InvocationTargetException e) {
			throw new OcmException(e);
		} catch (final NoSuchMethodException e) {
			throw new OcmException(e);
		}
	}


//	static QName createQname(final String qnameStr, final NamespaceRegistry nsreg) throws RepositoryException {
//		final String[] qname = StringUtils.split(qnameStr, ':');
//		return new QName(nsreg.getURI(qname[0]), qname[1]);
//	}

	static Name createName(final NameFactory nameFactory, final NamespaceRegistry nsreg, final String qnameStr) throws RepositoryException {
		final String[] qname = StringUtils.split(qnameStr, ':');
		return nameFactory.create(nsreg.getURI(qname[0]), qname[1]);
	}

	public static void dump(final Object object, final int level) {
		final String padding = StringUtils.repeat("  ", level);
		System.out.print(padding);

		if (object == null) {
			System.out.println("NULL");
			return;
		}

		System.out.println("CLASS: " + object.getClass().getCanonicalName());

		if (object.getClass().isArray()) {
			for (int i = 0; i < Array.getLength(object); i++) {
				dump(Array.get(object, i), level + 1);
			}
		} else if (object.getClass().isPrimitive() || object instanceof String || object instanceof Class || object instanceof Number || object instanceof Byte || object instanceof Character || object instanceof Boolean) {
			System.out.println(padding + "OBJ: " + object.toString());
		} else if (object instanceof Iterable) {
			for (final Object item : (Iterable<?>) object) {
				dump(item, level + 1);
			}
		} else if (object instanceof Map) {
			for (final Map.Entry<?, ?> entry : ((Map<?, ?>) object).entrySet()) {
				System.out.println(padding + "MAP-KEY: ");
				dump(entry.getKey(), level + 1);
				System.out.println(padding + "MAP-VAL: ");
				dump(entry.getValue(), level + 1);
			}
		} else {
			try {
				final Map<?, ?> map = PropertyUtils.describe(object);
				for (final Map.Entry<?, ?> entry : map.entrySet()) {
					final Object key = entry.getKey();
					if (!key.equals("class")) {
						System.out.println(padding + "PROP: " + key);
						dump(entry.getValue(), level + 1);
					}
				}
			} catch (final IllegalAccessException e) {
				e.printStackTrace();
			} catch (final InvocationTargetException e) {
				e.printStackTrace();
			} catch (final NoSuchMethodException e) {
				e.printStackTrace();
			}
		}

	}


	static void removeAllNodes(final Node node, final String name) throws RepositoryException {
		for (final NodeIterator ni = name == null ? node.getNodes() : node.getNodes(name); ni.hasNext(); ) {
			ni.nextNode().remove();
		}
	}


	static void removeProperty(final Node node, final String name) throws RepositoryException {
		if (node.hasProperty(name)) {
			node.getProperty(name).remove();
		}
	}


	public static void removeAllProperties(final Node node) throws RepositoryException {
		for (final PropertyIterator pi = node.getProperties(); pi.hasNext(); ) {
			final Property property = pi.nextProperty();
			if (!property.getDefinition().isProtected()) {
				property.remove();
			}
		}
	}


	public static int getJcrPropertyType(final Class<?> clazz) {
		if (String.class.isAssignableFrom(clazz)) {
			return PropertyType.STRING;
		} else if (Long.class.isAssignableFrom(clazz) || long.class.isAssignableFrom(clazz) ||
				Integer.class.isAssignableFrom(clazz) || int.class.isAssignableFrom(clazz) ||
				Short.class.isAssignableFrom(clazz) || short.class.isAssignableFrom(clazz)) {
			return PropertyType.LONG;
		} else if (Double.class.isAssignableFrom(clazz) || double.class.isAssignableFrom(clazz) ||
				Float.class.isAssignableFrom(clazz) || float.class.isAssignableFrom(clazz)) {
			return PropertyType.DOUBLE;
		} else if (Date.class.isAssignableFrom(clazz) || Calendar.class.isAssignableFrom(clazz)) {
			return PropertyType.DATE;
		} else if (byte[].class.isAssignableFrom(clazz) || InputStream.class.isAssignableFrom(clazz)) {
			return PropertyType.BINARY;
		} else if (Boolean.class.isAssignableFrom(clazz) || boolean.class.isAssignableFrom(clazz)) {
			return PropertyType.BOOLEAN;
		} else if (Enum.class.isAssignableFrom(clazz)) {
			return PropertyType.STRING;
		} else {
			throw new IllegalArgumentException("unsupported JCR primitive type: " + clazz);
		}
	}


}
